<?php
/**
 * Main class file
 *
 * @package PhpSecInfo
 * @author Ed Finkler <coj@funkatron.com>
 */


/**
 * The default language setting if none is set/retrievable
 *
 */
define ('PHPSECINFO_LANG_DEFAULT', 'en');

/**
 * a general version string to differentiate releases
 *
 */
define ('PHPSECINFO_VERSION', '0.2.2');

/**
 * a YYYYMMDD date string to indicate "build" date
 *
 */
define ('PHPSECINFO_BUILD', '20080723');

/**
 * Homepage for phpsecinfo project
 *
 */
define ('PHPSECINFO_URL', 'http://phpsecinfo.com');

/**
 * The base folder where views are stored.  Include trailing slash
 *
 */
define('PHPSECINFO_VIEW_DIR_DEFAULT', 'View/');


/**
 * The default format, used to load the proper view.
 */
define('PHPSECINFO_FORMAT_DEFAULT', 'Html');


/**
 * The base directory, used to resolve requires and includes
 */
define('PHPSECINFO_BASE_DIR', dirname(__FILE__));

/**
 * This is the main class for the phpsecinfo system.  It's responsible for
 * dynamically loading tests, running those tests, and generating the results
 * output
 *
 * Example:
 * <code>
 * <?php require_once(PHPSECINFO_BASE_DIR.'/PhpSecInfo.php'); ?>
 * <?php phpsecinfo(); ?>
 * </code>
 *
 * If you want to capture the output, or just grab the test results and display them
 * in your own way, you'll need to do slightly more work.
 *
 * Example:
 * <code>
 * require_once(PHPSECINFO_BASE_DIR.'/PhpSecInfo.php');
 * // instantiate the class
 * $psi = new PhpSecInfo();
 *
 * // load and run all tests
 * $psi->loadAndRun();
 *
 * // grab the results as a multidimensional array
 * $results = $psi->getResultsAsArray();
 * echo "<pre>"; echo print_r($results, true); echo "</pre>";
 *
 * // grab the standard results output as a string
 * $html = $psi->getOutput();
 *
 * // send it to the browser
 * echo $html;
 * </code>
 *
 *
 * The procedural function "phpsecinfo" is defined below this class.
 * @see phpsecinfo()
 *
 * @author Ed Finkler <coj@funkatron.com>
 *
 * see CHANGELOG for changes
 *
 */
class PhpSecInfo
{

    /**
     * An array of tests to run
     *
     * @var array PhpSecInfo_Test
     */
    var $tests_to_run = array();


    /**
     * An array of results.  Each result is an associative array:
     * <code>
     * $result['result'] = PHPSECINFO_TEST_RESULT_NOTICE;
     * $result['message'] = "a string describing the test results and what they mean";
     * </code>
     *
     * @var array
     */
    var $test_results = array();


    /**
     * An array of tests that were not run
     *
     * <code>
     * $result['result'] = PHPSECINFO_TEST_RESULT_NOTRUN;
     * $result['message'] = "a string explaining why the test was not run";
     * </code>
     *
     * @var array
     */
    var $tests_not_run = array();


    /**
     * The language code used.  Defaults to PHPSECINFO_LANG_DEFAULT, which
     * is 'en'
     *
     * @var string
     * @see PHPSECINFO_LANG_DEFAULT
     */
    var $language = PHPSECINFO_LANG_DEFAULT;


    /**
     * An array of integers recording the number of test results in each category.  Categories can include
     * some or all of the PHPSECINFO_TEST_* constants.  Constants are the keys, # of results are the values.
     *
     * @var array
     */
    var $result_counts = array();


    /**
     * The number of tests that have been run
     *
     * @var integer
     */
    var $num_tests_run = 0;


    /**
     * The base directory for phpsecinfo. Set within the constructor. Paths are resolved from this.
     * @var string
     */
    var $_base_dir;


    /**
     * The directory PHPSecInfo will look for views.  It defaults to the value
     * in PHPSECINFO_VIEW_DIR_DEFAULT, but can be changed with the setViewDirectory()
     * method.
     *
     * @var string
     */
    var $_view_directory;


    /**
     * The output format, used to load the proper view
     *
     * @var string
     **/
    var $_format;

    /**
     * Constructor
     *
     * @return PhpSecInfo
     */
    function PhpSecInfo($opts = null)
    {

        $this->_base_dir = dirname(__FILE__);

        if ($opts) {
            if (isset($opts['view_directory'])) {
                $this->setViewDirectory($opts['view_directory']);
            } else {
                $this->setViewDirectory(dirname(__FILE__) . DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT);
            }

            if (isset($opts['format'])) {
                $this->setFormat($opts['format']);
            } else {
                if (!strcasecmp(PHP_SAPI, 'cli')) {
                    $this->setFormat('Cli');
                } else {
                    $this->setFormat(PHPSECINFO_FORMAT_DEFAULT);
                }
            }

        } else { /* Use defaults */
            $this->setViewDirectory(dirname(__FILE__) . DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT);
            if (!strcasecmp(PHP_SAPI, 'cli')) {
                $this->setFormat('Cli');
            } else {
                $this->setFormat(PHPSECINFO_FORMAT_DEFAULT);
            }
        }
    }


    /**
     * recurses through the Test subdir and includes classes in each test group subdir,
     * then builds an array of classnames for the tests that will be run
     *
     */
    function loadTests()
    {

        $test_root = dir(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Test');

        //echo "<pre>"; echo print_r($test_root, true); echo "</pre>";

        while (false !== ($entry = $test_root->read())) {
            if (is_dir($test_root->path . DIRECTORY_SEPARATOR . $entry) && !preg_match('~^(\.|_vti)(.*)$~', $entry)) {
                $test_dirs[] = $entry;
            }
        }
        //echo "<pre>"; echo print_r($test_dirs, true); echo "</pre>";

        // include_once all files in each test dir
        foreach ($test_dirs as $test_dir) {
            $this_dir = dir($test_root->path . DIRECTORY_SEPARATOR . $test_dir);

            while (false !== ($entry = $this_dir->read())) {
                if (!is_dir($this_dir->path . DIRECTORY_SEPARATOR . $entry)) {
                    include_once $this_dir->path . DIRECTORY_SEPARATOR . $entry;
                    $classNames[] = "PhpSecInfo_Test_" . $test_dir . "_" . basename($entry, '.php');
                }
            }

        }

        // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here
        $this->tests_to_run = $classNames;
    }


    /**
     * This runs the tests in the tests_to_run array and
     * places returned data in the following arrays/scalars:
     * - $this->test_results
     * - $this->result_counts
     * - $this->num_tests_run
     * - $this->tests_not_run;
     *
     */
    function runTests()
    {
        // initialize a bunch of arrays
        $this->test_results = array();
        $this->result_counts = array();
        $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN] = 0;
        $this->num_tests_run = 0;

        foreach ($this->tests_to_run as $testClass) {

            /**
             * @var $test PhpSecInfo_Test
             */
            $test = new $testClass();

            if ($test->isTestable()) {
                $test->test();
                $rs = array('result'            => $test->getResult(),
                            'message'           => $test->getMessage(),
                            'value_current'     => $test->getCurrentTestValue(),
                            'value_recommended' => $test->getRecommendedTestValue(),
                            'moreinfo_url'      => $test->getMoreInfoURL(),
                );
                $this->test_results[$test->getTestGroup()][$test->getTestName()] = $rs;

                // initialize if not yet set
                if (!isset ($this->result_counts[$rs['result']])) {
                    $this->result_counts[$rs['result']] = 0;
                }

                $this->result_counts[$rs['result']]++;
                $this->num_tests_run++;
            } else {
                $rs = array('result'            => $test->getResult(),
                            'message'           => $test->getMessage(),
                            'value_current'     => NULL,
                            'value_recommended' => NULL,
                            'moreinfo_url'      => $test->getMoreInfoURL(),
                );
                $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN]++;
                $this->tests_not_run[$test->getTestGroup() . "::" . $test->getTestName()] = $rs;
            }
        }
    }


    /**
     * This is the main output method.  The look and feel mimics phpinfo()
     *
     */
    function renderOutput($page_title = "Security Information About PHP")
    {
        /**
         * We need to use PhpSecInfo_Test::getBooleanIniValue() below
         * @see PhpSecInfo_Test::getBooleanIniValue()
         */
        if (!class_exists('PhpSecInfo_Test')) {
            include(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR . 'Test.php');
        }
        $this->loadView($this->_format);
    }


    /**
     * This is a helper method that makes it easy to output tables of test results
     * for a given test group
     *
     * @param string $group_name
     * @param array $group_results
     */
    function _outputRenderTable($group_name, $group_results)
    {

        // exit out if $group_results was empty or not an array.  This sorta seems a little hacky...
        if (!is_array($group_results) || sizeof($group_results) < 1) {
            return false;
        }

        ksort($group_results);

        $this->loadView($this->_format . '/Result', array('group_name' => $group_name, 'group_results' => $group_results));

        return true;
    }


    /**
     * This outputs a table containing a summary of the test results (counts and % in each result type)
     *
     * @see PHPSecInfo::_outputRenderTable()
     * @see PHPSecInfo::_outputGetResultTypeFromCode()
     */
    function _outputRenderStatsTable()
    {

        foreach ($this->result_counts as $code => $val) {
            if ($code != PHPSECINFO_TEST_RESULT_NOTRUN) {
                $percentage = round($val / $this->num_tests_run * 100, 2);
                $result_type = $this->_outputGetResultTypeFromCode($code);
                $stats[$result_type] = array('count'   => $val,
                                             'result'  => $code,
                                             'message' => "$val out of {$this->num_tests_run} ($percentage%)");
            }
        }

        $this->_outputRenderTable('Test Results Summary', $stats);

    }


    /**
     * This outputs a table containing a summary or test that were not executed, and the reasons why they were skipped
     *
     * @see PHPSecInfo::_outputRenderTable()
     */
    function _outputRenderNotRunTable()
    {

        $this->_outputRenderTable('Tests Not Run', $this->tests_not_run);

    }


    /**
     * This is a helper function that returns a CSS class corresponding to
     * the result code the test returned.  This allows us to color-code
     * results
     *
     * @param integer $code
     * @return string
     */
    function _outputGetCssClassFromResult($code)
    {

        switch ($code) {
            case PHPSECINFO_TEST_RESULT_OK:
                return 'value-ok';
                break;

            case PHPSECINFO_TEST_RESULT_NOTICE:
                return 'value-notice';
                break;

            case PHPSECINFO_TEST_RESULT_WARN:
                return 'value-warn';
                break;

            case PHPSECINFO_TEST_RESULT_NOTRUN:
                return 'value-notrun';
                break;

            case PHPSECINFO_TEST_RESULT_ERROR:
                return 'value-error';
                break;

            default:
                return 'value-notrun';
                break;
        }

    }


    /**
     * This is a helper function that returns a label string corresponding to
     * the result code the test returned.  This is mainly used for the Test
     * Results Summary table.
     *
     * @see PHPSecInfo::_outputRenderStatsTable()
     * @param integer $code
     * @return string
     */
    function _outputGetResultTypeFromCode($code)
    {

        switch ($code) {
            case PHPSECINFO_TEST_RESULT_OK:
                return 'Pass';
                break;

            case PHPSECINFO_TEST_RESULT_NOTICE:
                return 'Notice';
                break;

            case PHPSECINFO_TEST_RESULT_WARN:
                return 'Warning';
                break;

            case PHPSECINFO_TEST_RESULT_NOTRUN:
                return 'Not Run';
                break;

            case PHPSECINFO_TEST_RESULT_ERROR:
                return 'Error';
                break;

            default:
                return 'Invalid Result Code';
                break;
        }

    }


    /**
     * Loads and runs all the tests
     *
     * As loading, then running, is a pretty common process, this saves a extra method call
     *
     * @since 0.1.1
     *
     */
    function loadAndRun()
    {
        $this->loadTests();
        $this->runTests();
    }


    /**
     * returns an associative array of test data.  Four keys are set:
     * - test_results  (array)
     * - tests_not_run (array)
     * - result_counts (array)
     * - num_tests_run (integer)
     *
     * note that this must be called after tests are loaded and run
     *
     * @since 0.1.1
     * @return array
     */
    function getResultsAsArray()
    {
        $results = array();

        $results['test_results'] = $this->test_results;
        $results['tests_not_run'] = $this->tests_not_run;
        $results['result_counts'] = $this->result_counts;
        $results['num_tests_run'] = $this->num_tests_run;

        return $results;
    }


    /**
     * returns the standard output as a string instead of echoing it to the browser
     *
     * note that this must be called after tests are loaded and run
     *
     * @since 0.1.1
     *
     * @return string
     */
    function getOutput()
    {
        ob_start();
        $this->renderOutput();
        $output = ob_get_clean();
        return $output;
    }


    /**
     * A very, very simple "view" system
     *
     */
    function loadView($view_name, $data = null)
    {
        if ($data != null) {
            extract($data);
        }

        $view_file = $this->getViewDirectory() . $view_name . ".php";

        if (file_exists($view_file) && is_readable($view_file)) {
            ob_start();
            include $view_file;
            echo ob_get_clean();
        } else {
            user_error("The view '{$view_file}' either does not exist or is not readable", E_USER_WARNING);
        }


    }


    /**
     * Returns the current view directory
     *
     * @return string
     */
    function getViewDirectory()
    {
        return $this->_view_directory;
    }


    /**
     * Sets the directory that PHPSecInfo will look in for views
     *
     * @param string $newdir
     */
    function setViewDirectory($newdir)
    {
        $this->_view_directory = $newdir;
    }


    function getFormat()
    {
        return $this->_format;
    }


    function setFormat($format)
    {
        $this->_format = $format;
    }

}


/**
 * A globally-available function that runs the tests and creates the result page
 *
 */
function phpsecinfo()
{
    // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here
    $psi = new PhpSecInfo();
    $psi->loadAndRun();
    $psi->renderOutput();
}

